/**
 * Copyright (c) 2021-2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "plugins/ecmascript/runtime/base/error_helper.h"
#include "include/stack_walker.h"
#include "plugins/ecmascript/runtime/base/builtins_base.h"
#include "plugins/ecmascript/runtime/base/error_type.h"
#include "plugins/ecmascript/runtime/ecma_macros.h"
#include "plugins/ecmascript/runtime/ecma_vm.h"
#include "plugins/ecmascript/runtime/global_env.h"
#include "plugins/ecmascript/runtime/interpreter/interpreter.h"
#include "plugins/ecmascript/runtime/js_object-inl.h"
#include "plugins/ecmascript/runtime/js_tagged_value-inl.h"
#include "plugins/ecmascript/runtime/object_factory.h"
#include "plugins/ecmascript/runtime/tooling/pt_js_extractor.h"

namespace ark::ecmascript::base {
using ark::tooling::ecmascript::PtJSExtractor;

JSTaggedValue ErrorHelper::ErrorCommonToString(EcmaRuntimeCallInfo *argv, const ErrorType &errorType)
{
    ASSERT(argv);
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. Let O be the this value.
    // 2. If Type(O) is not Object, throw a TypeError exception
    JSHandle<JSTaggedValue> thisValue = builtins_common::GetThis(argv);
    if (!thisValue->IsECMAObject()) {
        THROW_TYPE_ERROR_AND_RETURN(thread, "ErrorToString:not an object", JSTaggedValue::Exception());
    }
    // 3. Let name be Get(O, "name").
    // 4. ReturnIfAbrupt(name).
    auto globalConst = thread->GlobalConstants();
    JSHandle<JSTaggedValue> handleName = globalConst->GetHandledNameString();
    JSHandle<JSTaggedValue> name = JSObject::GetProperty(thread, thisValue, handleName).GetValue();
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 5. If name is undefined, let name be "Error"; otherwise let name be ToString(name).
    // 6. ReturnIfAbrupt(name).
    name = ErrorHelper::GetErrorName(thread, name, errorType);
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 7. Let msg be Get(O, "message").
    // 8. ReturnIfAbrupt(msg).
    JSHandle<JSTaggedValue> handleMsg = globalConst->GetHandledMessageString();
    JSHandle<JSTaggedValue> msg = JSObject::GetProperty(thread, thisValue, handleMsg).GetValue();
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 9. If msg is undefined, let msg be the empty String; otherwise let msg be ToString(msg).
    // 10. ReturnIfAbrupt(msg).
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    if (msg->IsUndefined()) {
        msg = JSHandle<JSTaggedValue>::Cast(factory->GetEmptyString());
    } else {
        msg = JSHandle<JSTaggedValue>::Cast(JSTaggedValue::ToString(thread, msg));
    }
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 11. If name is the empty String, return msg.
    // 12. If msg is the empty String, return name.
    if (JSHandle<EcmaString>::Cast(name)->GetLength() == 0) {
        return msg.GetTaggedValue();
    }

    if (JSHandle<EcmaString>::Cast(msg)->GetLength() == 0) {
        return name.GetTaggedValue();
    }

    // 13. Return the result of concatenating name, the code unit 0x003A (COLON), the code unit 0x0020 (SPACE), and msg.
    JSHandle<EcmaString> space = factory->NewFromCanBeCompressString(": ");
    JSHandle<EcmaString> jsHandleName = JSHandle<EcmaString>::Cast(name);
    JSHandle<EcmaString> jsHandleMsg = JSHandle<EcmaString>::Cast(msg);
    JSHandle<EcmaString> handleNameSpace = factory->ConcatFromString(jsHandleName, space);
    JSHandle<EcmaString> result = factory->ConcatFromString(handleNameSpace, jsHandleMsg);
    return result.GetTaggedValue();
}

JSHandle<JSTaggedValue> ErrorHelper::GetErrorName(JSThread *thread, const JSHandle<JSTaggedValue> &name,
                                                  const ErrorType &errorType)
{
    auto globalConst = thread->GlobalConstants();
    if (name->IsUndefined()) {
        switch (errorType) {
            case ErrorType::RANGE_ERROR:
                return globalConst->GetHandledRangeErrorString();
            case ErrorType::EVAL_ERROR:
                return globalConst->GetHandledEvalErrorString();
            case ErrorType::REFERENCE_ERROR:
                return globalConst->GetHandledReferenceErrorString();
            case ErrorType::TYPE_ERROR:
                return globalConst->GetHandledTypeErrorString();
            case ErrorType::URI_ERROR:
                return globalConst->GetHandledURIErrorString();
            case ErrorType::SYNTAX_ERROR:
                return globalConst->GetHandledSyntaxErrorString();
            default:
                return globalConst->GetHandledErrorString();
        }
    }
    return JSHandle<JSTaggedValue>::Cast(JSTaggedValue::ToString(thread, name));
}

JSTaggedValue ErrorHelper::ErrorCommonConstructor(EcmaRuntimeCallInfo *argv,
                                                  [[maybe_unused]] const ErrorType &errorType)
{
    JSThread *thread = argv->GetThread();
    [[maybe_unused]] EcmaHandleScope handleScope(thread);

    // 1. If NewTarget is undefined, let new_target be the active function object, else let new_target be NewTarget.
    auto ecmaVm = thread->GetEcmaVM();
    ObjectFactory *factory = ecmaVm->GetFactory();
    JSHandle<JSTaggedValue> ctor = builtins_common::GetConstructor(argv);
    JSMutableHandle<JSTaggedValue> newTarget(builtins_common::GetNewTarget(argv));
    if (newTarget->IsUndefined()) {
        newTarget.Update(ctor.GetTaggedValue());
    }
    JSHandle<JSTaggedValue> message = builtins_common::GetCallArg(argv, 0);

    // 2. Let O be OrdinaryCreateFromConstructor(new_target, "%ErrorPrototype%", «[[ErrorData]]»).
    JSHandle<JSObject> nativeInstanceObj = factory->NewJSObjectByConstructor(JSHandle<JSFunction>(ctor), newTarget);

    // 3. ReturnIfAbrupt(O).
    RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);

    // 4. If message is not undefined, then
    //    a. Let msg be ToString(message).
    //    b. ReturnIfAbrupt(msg).
    //    c. Let msgDesc be the PropertyDescriptor{[[Value]]: msg, [[Writable]]: true, [[Enumerable]]: false,
    //       [[Configurable]]: true}.
    //    d. Let status be DefinePropertyOrThrow(O, "message", msgDesc).
    //    e. Assert: status is not an abrupt completion
    auto globalConst = thread->GlobalConstants();
    if (!message->IsUndefined()) {
        JSHandle<EcmaString> handleStr = JSTaggedValue::ToString(thread, message);
        LOG(DEBUG, ECMASCRIPT) << "Ark throw error: " << utf::Mutf8AsCString(handleStr->GetDataUtf8());
        RETURN_EXCEPTION_IF_ABRUPT_COMPLETION(thread);
        JSHandle<JSTaggedValue> msgKey = globalConst->GetHandledMessageString();
        PropertyDescriptor msgDesc(thread, JSHandle<JSTaggedValue>::Cast(handleStr), true, false, true);
        [[maybe_unused]] bool status = JSObject::DefineOwnProperty(thread, nativeInstanceObj, msgKey, msgDesc);
        ASSERT_PRINT(status, "return result exception!");
    }

    JSHandle<EcmaString> handleStack = BuildEcmaStackTrace(thread);
    JSHandle<JSTaggedValue> stackkey = globalConst->GetHandledStackString();
    PropertyDescriptor stackDesc(thread, JSHandle<JSTaggedValue>::Cast(handleStack), true, false, true);
    [[maybe_unused]] bool status = JSObject::DefineOwnProperty(thread, nativeInstanceObj, stackkey, stackDesc);
    ASSERT_PRINT(status, "return result exception!");

    // 5. Return O.
    return nativeInstanceObj.GetTaggedValue();
}

PandaString ErrorHelper::DecodeFunctionName(const PandaString &name)
{
    if (name.empty()) {
        return "anonymous";
    }
    return name;
}

JSHandle<EcmaString> ErrorHelper::BuildEcmaStackTrace(JSThread *thread)
{
    PandaString data = BuildNativeEcmaStackTrace(thread);
    ObjectFactory *factory = thread->GetEcmaVM()->GetFactory();
    LOG(DEBUG, ECMASCRIPT) << data;
    return factory->NewFromString(data);
}

PandaString ErrorHelper::BuildNativeEcmaStackTrace(JSThread *thread)
{
    auto ecmaVm = thread->GetEcmaVM();
    PandaString data;

    for (auto stack = StackWalker::Create(thread); stack.HasFrame(); stack.NextFrame()) {
        auto method = stack.GetMethod();
        if (method->IsNative()) {
            data += INTRINSIC_METHOD_NAME;
        } else {
            data.append("    at ");
            data += DecodeFunctionName(JSMethod::Cast(method)->ParseFunctionName());
            data.append(" (");
            // source file
            PtJSExtractor *debugExtractor = ecmaVm->GetDebugInfoExtractor(method->GetPandaFile());
            const PandaString &sourceFile = debugExtractor->GetSourceFile(method->GetFileId());
            if (sourceFile.empty()) {
                data.push_back('?');
            } else {
                data += sourceFile;
            }
            data.push_back(':');
            // line number and column number
            auto callbackFunc = [&data](size_t line, size_t column) -> bool {
                data += ToPandaString(line + 1);
                data.push_back(':');
                data += ToPandaString(column + 1);
                return true;
            };
            if (!debugExtractor->MatchWithOffset(callbackFunc, method->GetFileId(), stack.GetBytecodePc())) {
                data.push_back('?');
            }
            data.push_back(')');
        }
        data.push_back('\n');
    }

    return data;
}
}  // namespace ark::ecmascript::base
